/*****
 * TODO:
 * 	implement sceduling
 */


#include "types.h"
#include "user.h"
#include "uthread.h"
#include "spinlock.h"


struct {
    uthread_t* threads[MAX_UTHREADS];
    struct spinlock lock;
} ttable;


int threadsHaveBeenStarted_ = 0;
int nextTID_ = 0;
int currentThreadIndex_ = 0;
//int threadNum_ = -1;

int uthread_create(void (*start_func)(), int priority){
    uthread_t** ut;
    uthread_t* t;
    //  printf(1, "- creating a thread, its tid will be: %d\n", nextTID_);
    for(ut = ttable.threads; ut < &ttable.threads[MAX_UTHREADS]; ut++){
        if(*ut == 0 || (*ut)->state == UNUSED)
            break;
    }
    if(ut == &ttable.threads[MAX_UTHREADS]){
        printf(1, "\nerror: the thread table is full, can't add another one\n");
        return -1;
    }
    //threadNum_++;
    
    t = (uthread_t*)malloc(sizeof(uthread_t));
    t->ss_theStack = malloc(STACK_SIZE);
    t->ss_sp = t->ss_theStack;	// for a future free
    t->ss_sp = t->ss_sp + STACK_SIZE;		// because addresses are managed from the end
    t->ss_bp = t->ss_sp;
    
    t->tid = nextTID_;			// A unique thread ID within the process
    t->ss_size = STACK_SIZE;		// Stack size
    t->priority = priority;
    t->state = EMBRYO;
    t->start = start_func;
    
    *ut = t;	// save new thrad to ttable
    nextTID_++;	// prepare for next time
    
    void* oldesp;
    //pushing the init function into the threads stack
    STORE_ESP(oldesp);
    LOAD_ESP(t->ss_sp);
    PUSH(start_func);
    STORE_ESP(t->ss_sp);
    LOAD_ESP(oldesp);
    // //   printf(1, "-+-+-finished: sp: %d, bp: %d\n", t->ss_sp, t->ss_bp);
    
    return t->tid;
}

int getNextThreadRR(){
    //printf(1, "the current thread index is %d\n",currentThreadIndex_);
    uthread_t** ut = &ttable.threads[currentThreadIndex_];

    
    //choose the next thread based on the defined policy: SCHED_RR
    //store this thread and the next thread
    //if a new thread comes he is the last thread
    int tempThreadLocation = currentThreadIndex_;
    int flag = 0;
    while (flag == 0) {
        ut++;
        tempThreadLocation++;
        
        if (ut == &ttable.threads[MAX_UTHREADS]) {
            ut = ttable.threads;
            tempThreadLocation = 0;
        }
        if ((*ut) != 0 && ((*ut)->state == RUNNABLE || (*ut)->state == EMBRYO)){
//            currentThreadIndex_ = tempThreadLocation;
            //printf(1, "the chosen thread index is %d\n",currentThreadIndex_);
            if (currentThreadIndex_ != tempThreadLocation) {
                break;
            }
        }
    }
    
    //release lock
    if (ut == &ttable.threads[MAX_UTHREADS]){
        printf(2,"\nerror: no thread to run\n");
        return -1;
    }
    
    return tempThreadLocation;
}



int getNextThreadPB(){
    int topPriority;
    int flag = 0;
    uthread_t** ut;

    //finding the top priority --
    //you can't do it in the next "while"
    //because you need to go through all the table before moving to the next priority
    for (topPriority = 0; topPriority<10; topPriority++) {
        for(ut=ttable.threads; ut < &ttable.threads[MAX_UTHREADS]; ut++){
            if((*ut) != 0 && ((*ut)->state == EMBRYO || (*ut)->state == RUNNABLE || (*ut)->state == RUNNING)){
                //printf(1, "Finding Top Priority:: tid: %d state%d priority: %d\n",(*ut)->tid ,(*ut)->state, (*ut)->priority);
                if ((*ut)->priority == topPriority) {
                    flag = 1;
                    break;
                }
            }
        }
        if (flag == 1) {
            break;
        }
    }
    //printf(1, "top priority is %d\n",topPriority);
    
    //printf(1, "the current thread index is %d\n",currentThreadIndex_);
    ut = &ttable.threads[currentThreadIndex_];
    
    //choose the next thread based on the defined policy:SCHED_PB
    //store this thread and the next thread
    //if a new thread comes he is the last thread
    int tempThreadLocation = currentThreadIndex_;
    flag = 0;
    int aloneInTopPriority = 0;
    while (flag == 0) {
        ut++;
        tempThreadLocation++;
        
        if (ut == &ttable.threads[MAX_UTHREADS]) {
            ut = ttable.threads;
            tempThreadLocation = 0;
        }
        if ((*ut) != 0 && ((*ut)->state == RUNNABLE || (*ut)->state == EMBRYO)){
            //            currentThreadIndex_ = tempThreadLocation;
            if ((*ut)->priority == topPriority) {
                //printf(1, "the chosen thread index is %d, his proiorty is %d\n",tempThreadLocation, (*ut)->priority);
                flag = 1;
                break;
            }
        }else if((*ut) != 0 && ((*ut)->state == RUNNING)){
            if ((*ut)->priority == topPriority) {
                if (aloneInTopPriority == 1) {
                    //printf(1, "Forever Alone: the chosen thread index is %d, his proiorty is %d\n",tempThreadLocation, (*ut)->priority);
                    flag = 1;
                    break;                    
                }else{
                    aloneInTopPriority++;
                }
            }
        }
    }
    
    //release lock
    if (ut == &ttable.threads[MAX_UTHREADS]){
        printf(2,"\nerror: no thread to run\n");
        return -1;
    }
    
    return tempThreadLocation;
}


void uthread_yield(){
    uthread_t** ut;
    int newThreadIndex = 0;
    //  printf(1, "- yield starting\n");
    
//    uthread_t** t;
//    for(t=ttable.threads; t < &ttable.threads[MAX_UTHREADS]; t++){
//        printf(1, "state: %d tid: %d\n", (*t)->state, (*t)->tid);
//    }
    
#ifdef SCHED_PB
    newThreadIndex = getNextThreadPB();//PB
#else
    newThreadIndex = getNextThreadRR();//RR
#endif
    
    //old thread
    if((ttable.threads[currentThreadIndex_] != 0) && (threadsHaveBeenStarted_ != 0)){
        ttable.threads[currentThreadIndex_]->state = RUNNABLE;
        STORE_EBP(ttable.threads[currentThreadIndex_]->ss_bp);
        STORE_ESP(ttable.threads[currentThreadIndex_]->ss_sp);
    }
    threadsHaveBeenStarted_ = 1; //for making sure this is the first time --> no STORE
    
    //new thread
    ut = &ttable.threads[newThreadIndex];
    currentThreadIndex_ = newThreadIndex;
    
    //   ttable.threads[currentThreadIndex_] = *ut;
    //  printf(1, "- state %d tid: %d, i: %d\n", (*ut)->state, (*ut)->tid, i);
    if((*ut)->state == RUNNABLE){
        (*ut)->state = RUNNING;
            //printf(1, "- running RUNNABLE, tid: %d addr: %d\n", (*ut)->tid, *ut);
        LOAD_ESP((*ut)->ss_sp);
        LOAD_EBP((*ut)->ss_bp);
    }else if ((*ut)->state == EMBRYO){
            //printf(1, "- running EMBRYO, tid: %d\n", (*ut)->tid);
        (*ut)->state = RUNNING;
        LOAD_ESP((*ut)->ss_sp);
        LOAD_EBP((*ut)->ss_bp);
        CALL(wrap_function);
    }else{
        printf(2,"\nerror: should never get here!\n");
        exit();
    }
    //  printf(1, "- exit yield\n");
}


void uthread_exit(){
    uthread_t** ut;
    int flag = 0;
     // int finishedTID = ttable.threads[currentThreadIndex_]->tid;
    //threadNum_--;
    
    
    //releasing currentThreadIndex_
    free(ttable.threads[currentThreadIndex_]->ss_theStack);
    ttable.threads[currentThreadIndex_]->state = UNUSED;	//probably unnessisary, but better that an error
    free(ttable.threads[currentThreadIndex_]);
    
    //  acquire(&ttable.lock);
    for(ut=ttable.threads; ut < &ttable.threads[MAX_UTHREADS]; ut++){
//        if (*ut != 0 && (*ut)->state != UNUSED) {
//        printf(1, "pid: %d state%d\n",(*ut)->tid ,(*ut)->state);
        if((*ut) != 0 && ((*ut)->state == RUNNING || (*ut)->state == RUNNABLE || (*ut)->state == EMBRYO)){
            flag = 1;
            break;
        }
    }
    ttable.threads[currentThreadIndex_] = 0;
    //release lock
    if(flag == 0){
        //all threads have finished running. exit process
             //printf(1, "- all threads have finished, exitting process\n");
        exit();
    }else{
             //printf(1, "- thread %d has finished, callig uthread_yield()\n", finishedTID);
        uthread_yield();
    }
}

int uthread_start_all(){
//    uthread_t** t;
//    int i;
    //  printf(1, "- starting all threads\n");
    
    if(threadsHaveBeenStarted_ != 0){	// Making sure this is the first call
        printf(2, "can't call uthread_start_all more that once! please don't do that again...\n");
        return -1;
    }
    
    uthread_yield();
    
//    //  acquire(&ttable.lock);
//    for(t=ttable.threads, i=0; t < &ttable.threads[MAX_UTHREADS]; t++, i++){
//        if ((*t)->state == RUNNABLE || (*t)->state == EMBRYO) {
//            break;
//        }
//        
//    }
//    currentThreadIndex_=i;
//    //release lock
//    if (t == 0 || t == &ttable.threads[MAX_UTHREADS]){
//        printf(2, "Can't find a thread to start, exit\n");
//        return -1;
//    }
//    
//    //newThreadIndex = getNextThreadRR();//RR
//    newThreadIndex = getNextThreadPB();//PB
//    
//    //else: run the thread
//    //  printf(1, "- found thread with tid: %d state: %d\n", (*t)->tid, (*t)->state);
//    
//    //  here we should NOT store any stack, because this call should not return!
//    (*t)->state = RUNNING;
//    LOAD_ESP((*t)->ss_sp);
//    LOAD_EBP((*t)->ss_bp);
//    
//    CALL(wrap_function);
    
    return -1;
}


int uthread_setpr(int priority){
    int ret;
    if (ttable.threads[currentThreadIndex_] == 0) {
        printf(2, "currentThreadIndex_ is 0; have to call from inside the thread itself!\n");
        return -1;
    }
    if(priority < 0 || priority > 9){
        printf(2, "invalid priority: %d\n", priority);
        return -1;
    }
    
    ret = ttable.threads[currentThreadIndex_]->priority;
    ttable.threads[currentThreadIndex_]->priority = priority;
    return ret;
}


int uthread_getpr(){
    if(ttable.threads[currentThreadIndex_] == 0)
        return -1;
    return ttable.threads[currentThreadIndex_]->priority;
}


uthread_t uthread_self(){
    return *ttable.threads[currentThreadIndex_];
}

// A function that wraps the entry functipn of a thread.
// This is just a suggestion, fell free to modify it as needed.
void wrap_function(void (*entry)())
{
    //  printf(1, "- Enter wrap function\n");
    entry();
    //  printf(1, "- Thread with tid: %d has finished, calling uthread_exit()\n", ttable.threads[currentThreadIndex_]->tid);
    uthread_exit();
}
